package ubadbtools.deadlockAnalyzer;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import ubadbtools.deadlockAnalyzer.common.Schedule;
import ubadbtools.deadlockAnalyzer.scheduleRecords.ScheduleRecord;
import ubadbtools.deadlockAnalyzer.scheduleRecords.StartRecord;
import ubadbtools.deadlockAnalyzer.results.ValidationResult;

public class ScheduleValidation {
	public static ValidationResult executeFor(Schedule aSchedule) {
		return new ScheduleValidation(aSchedule).execute();
	}

	// private int timestampGlobal = 0;
	private Schedule schedule = null;
	private Set<String> started = new HashSet<String>();
	private Set<String> commited = new HashSet<String>();
	private Set<String> aborted = new HashSet<String>();
	
	private Set<String> itemsGlobalLocked = new HashSet<String>();
	private Map<String, Set<String>> itemsLockedByTransaction = new HashMap<String, Set<String>>();
	private Map<String, Set<String>> itemsRequestedLockByTransaction = new HashMap<String, Set<String>>();
	
	private Set<String> unlocked = new HashSet<String>();	// 2PL: Lista de Transacciones que comenzaron a realizar unlock
	private Set<String> transactionIs2PL = new HashSet<String>();	// 2PL: Lista de Transacciones que cumplen 2PL
	private Set<String> transactionWithUnlockBeforeCommitAbort = new HashSet<String>(); // S2PL: Lista de transacciones que realizaron un unlock antes del commit o abort
	private Set<String> transactionIsS2PL = new HashSet<String>();	// S2PL: Lista de transacciones que son S2PL
	
	public ScheduleValidation(Schedule aSchedule) {
		schedule = aSchedule;
	}

	private ValidationResult execute() {
		boolean passed = true;
		String message = "";

		try {
			for (ScheduleRecord sr : schedule.getScheduleRecords()) {
				processStart(sr);
				processCommit(sr);
				processAbort(sr);
				processLock(sr);
				processUnlock(sr);
				processRequestLock(sr);
			}
		} catch (ValidationException e) {
			message = e.getMessage();
			passed = false;
		}

		if (transactionIsS2PL.size()>0)
		{
			message += "Las siguientes transacciones son S2PL: ";
			for (Iterator<String> i = transactionIsS2PL.iterator(); i.hasNext();)
			{
				message += i.next() + " ";
			}
		}
		
		if (transactionIs2PL.size()>0)
		{
			message += "\nLas siguientes transacciones son 2PL: ";
			for (Iterator<String> i = transactionIs2PL.iterator(); i.hasNext();)
			{
				message += i.next() + " ";
			}
			
		}
		return new ValidationResult(passed, message);
	}

	private void processCommit(ScheduleRecord sr) throws ValidationException {
		if (!sr.isCommit())
			return;

		transactionIsNotDuplicated(commited, sr);
		transactionStartedBefore(sr);
		transactionIsNotAborted(sr);

		commited.add(sr.getTransaction());
		cleanUp(sr);
		
		// S2PL : si la transaccion no hizo ningun unlock antes del commit o abort
		//			entonces la transaccion es S2PL
		if (!transactionWithUnlockBeforeCommitAbort.contains(sr.getTransaction()))
		{
			transactionIsS2PL.add(sr.getTransaction());
		}

	}

	private void cleanUp(ScheduleRecord sr) {
		if (!itemsLockedByTransaction.containsKey(sr.getTransaction()))
			return;
		Set<String> setOfLocks = itemsLockedByTransaction.get(sr
				.getTransaction());
		for (String item : setOfLocks) {
			itemsGlobalLocked.remove(item);
		}

		itemsLockedByTransaction.remove(sr.getTransaction());
	}


	private void processRequestLock(ScheduleRecord sr)
			throws ValidationException {
		if (!sr.isRequestLock())
			return;

		Set<String> setOfItems = itemsRequestedLockByTransaction.get(sr
				.getTransaction());
		if (setOfItems == null)
			setOfItems = new HashSet<String>();

		transactionStartedBefore(sr);
		transactionIsNotAborted(sr);
		transactionIsNotCommited(sr);
		transactionNotRequestedBefore(sr);
		transactionNotLockedBefore(sr);
		setOfItems.add(sr.getItem());

		itemsRequestedLockByTransaction.put(sr.getTransaction(), setOfItems);
	
		// 2PL - si la transaccion ya habia comenzado con su fase de unlocks
		//		elimino la misma de la lista de transacciones 2PL
		if (unlocked.contains(sr.getTransaction()))
		{
			transactionIs2PL.remove(sr.getTransaction());
		}

	}

	private void processUnlock(ScheduleRecord sr) throws ValidationException {
		if (!sr.isUnlock())
			return;

		transactionStartedBefore(sr);
		transactionIsNotAborted(sr);
		transactionIsNotCommited(sr);
		transactionIsLocked(sr);

		// Agrego en unlocked la transaccion para registrar que empieza el
		// proceso
		// descendente de 2PL
		unlocked.add(sr.getTransaction());

		// Verifico si la transaccion no hizo commit/abort no es S2PL
		if (!commited.contains(sr.getTransaction()) && !aborted.contains(sr.getTransaction()))
		{
			transactionWithUnlockBeforeCommitAbort.add(sr.getTransaction());
		}

		doUnlock(sr);

	}

	private void processLock(ScheduleRecord sr) throws ValidationException {
		if (!sr.isLock())
			return;


		transactionStartedBefore(sr);
		transactionIsNotAborted(sr);
		transactionIsNotCommited(sr);
		transactionIsRequestedBeforeLock(sr);
		// no esta lockeada por si misma
		transactionIsNotLocked(sr);
		// el item esta lockeado por otra transaccion
		transactionNotGlobalLocked(sr);

		Set<String> setOfItemsRL = itemsRequestedLockByTransaction.get(sr
				.getTransaction());
		if (setOfItemsRL == null)
			setOfItemsRL = new HashSet<String>();

		setOfItemsRL.remove(sr.getItem());
		itemsRequestedLockByTransaction.put(sr.getTransaction(), setOfItemsRL);

		Set<String> setOfItemsL = itemsLockedByTransaction.get(sr
				.getTransaction());
		if (setOfItemsL == null)
			setOfItemsL = new HashSet<String>();

		setOfItemsL.add(sr.getItem());
		itemsLockedByTransaction.put(sr.getTransaction(), setOfItemsL);

		itemsGlobalLocked.add(sr.getItem());

		// 2PL - si la transaccion ya habia comenzado con su fase de unlocks
		//		elimino la misma de la lista de transacciones 2PL
		if (unlocked.contains(sr.getTransaction()))
		{
			transactionIs2PL.remove(sr.getTransaction());
		}
	}

	private void transactionNotGlobalLocked(ScheduleRecord sr)
			throws ValidationException {
		if (itemsGlobalLocked.contains(sr.getItem())) {
			fail("El item " + sr.getItem()
					+ " ya esta lockeado por otra transacción");
		}

	}

	private void processAbort(ScheduleRecord sr) throws ValidationException {
		if (!sr.isAbort())
			return;

		transactionIsNotDuplicated(aborted, sr);
		transactionStartedBefore(sr);
		transactionIsNotCommited(sr);

		aborted.add(sr.getTransaction());
		cleanUp(sr);
		
		// S2PL : si la transaccion no hizo ningun unlock antes del commit o abort
		//			entonces la transaccion es S2PL
		if (!transactionWithUnlockBeforeCommitAbort.contains(sr.getTransaction()))
		{
			transactionIsS2PL.add(sr.getTransaction());
		}
	}

	private void processStart(ScheduleRecord sr) throws ValidationException {
		if (!sr.isStart())
			return;

		transactionIsNotDuplicated(started, sr);

		started.add(sr.getTransaction());
		// sr.setTimestamp(timestampGlobal);
		// timestampGlobal = timestampGlobal +1;

		// 2PL : una transaccion comenzada en principio es 2PL si no posee operaciones
		transactionIs2PL.add(sr.getTransaction());
	}

	private void transactionIsLocked(ScheduleRecord sr)
			throws ValidationException {
		Set<String> conj = itemsLockedByTransaction.get(sr.getTransaction());
		if (conj == null || !conj.contains(sr.getItem())) {
			fail("El registro " + sr + " no es valido porque la transacción "
					+ sr.getTransaction() + " no hizo un lock para el item "
					+ sr.getItem());
		}
	}

	private void transactionIsRequestedBeforeLock(ScheduleRecord sr)
			throws ValidationException {
		Set<String> setOfItemRL = itemsRequestedLockByTransaction.get(sr
				.getTransaction());
		if (setOfItemRL == null || !setOfItemRL.contains(sr.getItem())) {
			fail("El registro " + sr + " no es valido porque la transacción "
					+ sr.getTransaction()
					+ " hizo un lock sin antes hacer un request del item "
					+ sr.getItem());
		}

	}

	private void transactionIsNotLocked(ScheduleRecord sr)
			throws ValidationException {
		Set<String> conj = itemsLockedByTransaction.get(sr.getTransaction());
		if (conj != null && conj.contains(sr.getItem())) {
			fail("El registro " + sr + " no es valido porque la transacción "
					+ sr.getTransaction() + " no hizo un lock para el item "
					+ sr.getItem());
		}

	}

	private void transactionNotLockedBefore(ScheduleRecord sr)
			throws ValidationException {
		Set<String> conj = itemsLockedByTransaction.get(sr.getTransaction());
		if (conj != null && conj.contains(sr.getItem())) {
			fail("El registro " + sr + " no es valido porque la transacción "
					+ sr.getTransaction() + " ya hizo un lock para el item "
					+ sr.getItem());
		}

	}

	private void transactionNotRequestedBefore(ScheduleRecord sr)
			throws ValidationException {
		Set<String> conj = itemsRequestedLockByTransaction.get(sr
				.getTransaction());
		if (conj != null && conj.contains(sr.getItem())) {
			fail("El registro " + sr + " no es valido porque la transacción "
					+ sr.getTransaction()
					+ " ya pidió un request para el item " + sr.getItem());
		}
	}

	private void transactionIsNotCommited(ScheduleRecord sr)
			throws ValidationException {
		if (commited.contains(sr.getTransaction())) {
			fail("El registro " + sr + " no es valido porque la transacción "
					+ sr.getTransaction() + " ya fue comiteada");
		}

	}

	private void transactionIsNotAborted(ScheduleRecord sr)
			throws ValidationException {
		if (aborted.contains(sr.getTransaction())) {
			fail("El registro " + sr + " no es valido porque la transacción "
					+ sr.getTransaction() + " ya fue abortada");
		}

	}

	private void transactionIsNotDuplicated(Set<String> aSet, ScheduleRecord sr)
			throws ValidationException {
		if (aSet.contains(sr.getTransaction())) {
			fail("Puede haber como máximo un " + sr + "en la transacción "
					+ sr.getTransaction());
		}

	}

	private void transactionStartedBefore(ScheduleRecord sr)
			throws ValidationException {
		if (!started.contains(sr.getTransaction())) {
			fail("La primera acción de " + sr.getTransaction() + " debe ser "
					+ new StartRecord(sr.getTransaction()));
		}
	}

	private void fail(String message) throws ValidationException {
		throw new ValidationException(message);
	}

	private void doUnlock(ScheduleRecord sr) throws ValidationException {
		Set<String> setOfItems = itemsLockedByTransaction.get(sr
				.getTransaction());
		if (setOfItems == null)
			setOfItems = new HashSet<String>();

		setOfItems.remove(sr.getItem());
		itemsLockedByTransaction.put(sr.getTransaction(), setOfItems);
		itemsGlobalLocked.remove(sr.getItem());
	}
}
